package controleur;

import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Observer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.locks.ReentrantLock;

import javax.swing.JOptionPane;
import javax.swing.SwingWorker;

import sim_moteur.Coordonnee;
import sim_moteur.Simulateur;
import sim_moteur.Simulateur.ChosesPossibles;
import sim_moteur.Terrain;
import sim_moteur.element.Chose;
import sim_moteur.element.Vivant;
import sim_moteur.element.vegetal.Vegetal;
import sim_moteur.factory.AbstractFactory;
import sim_moteur.factory.SimFactory;
import util.SimProperties;
import vue.Affichable;
import vue.AppWindow;
import vue.AppWindow.FournisseurDeChose;
import vue.XY;
import vue.event.InitEvent;
import vue.event.PauseEvent;
import vue.event.StartEvent;
import vue.event.StepEvent;
import vue.event.VueEvent;

public class Controleur {

	public final static String VEGETAUX_PROPERTY_NAME = "simulation.vegetaux.nombre";
	public final static String ANIMAUX_PROPERTY_NAME = "simulation.animal.nombre";
	public final static String CHOSE_PROPERTY_NAME = "simulation.chose.nombre";
	public final static String NOMBRE_SIM_PROPERTY_NAME = "simulation.nombre";

	private static boolean pause = false;
	private static boolean simulationCommencer = false;
	private static volatile boolean stop = false;
	private static Thread[] threadArray;
	private Thread threadFinale;
	private static Simulateur[] simArray;
	private int nombreSimulation;
	private static Object moniteurPause = new Object();
	private static Object moniteurSims = new Object();
	private static Object moniteurThread = new Object();

	AppWindow vue;
	Simulateur sim;// c'est le modèle
	FournisseurDeChose[] adapteurDeChose;

	public static void main(String[] args) {
		new Controleur().start();
	}

	/**
	 * Démarre l'application.
	 */
	private void start() {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					vue.montreVue();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	public Controleur() {
		super();

		int largeur = Terrain.getInstance().getXSize();
		int hauteur = Terrain.getInstance().getYSize();

		vue = new AppWindow(largeur, hauteur);

		/*
		 * Obtien le nombre de simulation
		 */
		nombreSimulation = SimProperties.getInstance().getIntProperty(
				NOMBRE_SIM_PROPERTY_NAME);

		/*
		 * Crée un tableu de thread et simulation respectif au nombre de
		 * simulation
		 */
		threadArray = new Thread[nombreSimulation];
		simArray = new Simulateur[nombreSimulation];
		adapteurDeChose = new FournisseurDeChose[nombreSimulation];

		/*
		 * Initi les simulateurs
		 */
		for (int i = 0; i < simArray.length; i++) {
			simArray[i] = new Simulateur();
		}

		// Le fournisseur de chose est l'alterego du simulateur pour l'affichage
		for (int i = 0; i < adapteurDeChose.length; i++) {

			// pour chaque simulateur, nous l'enregistrons à un fournisseur pour
			// que la vue mettre a jour les n simulateurs
			Simulateur simLocal = simArray[i];
			adapteurDeChose[i] = new FournisseurDeChose() {

				private <I extends Chose> Collection<Affichable> adapteChose(
						Collection<I> choses) {
					Collection<Affichable> retVal = new ArrayList<>(
							choses.size());
					for (I chose : choses) {
						String name = chose.getName();
						String contenu = chose.toString();
						boolean estVivant = chose.estVivant();
						Coordonnee coordonnee = chose.getCoordonnees();
						XY xy = new XY(coordonnee.getX(), coordonnee.getY());
						float vie = chose instanceof Vivant ? ((Vivant) chose)
								.getVie() : 0;
						retVal.add(new Affichable(name, contenu, estVivant, xy,
								vie));
					}
					return retVal;
				}

				@Override
				public Collection<Affichable> getVegetaux() {
					return adapteChose(simLocal.getVegetaux().values());
				}

				@Override
				public String getNameForIndex(int index) {
					return simLocal.getNameForIndex(index);
				}

				@Override
				public Collection<Affichable> getLesChosesBasiques() {
					return adapteChose(simLocal.getLesChosesBasiques().values());
				}

				@Override
				public Collection<Affichable> getAnimaux() {
					return adapteChose(simLocal.getAnimaux().values());
				}

				@Override
				public Affichable getChose(String name) {
					Affichable retVal = null;
					Chose chose = simLocal.getChose(name);
					if (chose != null) {

						String contenu = chose.toString();
						boolean estVivant = chose instanceof Vivant ? ((Vivant) chose)
								.estVivant() : false;
						Coordonnee coordonnee = chose.getCoordonnees();
						XY xy = new XY(coordonnee.getX(), coordonnee.getY());
						float vie = chose instanceof Vivant ? ((Vivant) chose)
								.getVie() : 0f;
						retVal = new Affichable(name, contenu, estVivant, xy,
								vie);
					}
					return retVal;
				}
			};
		}

		vue.addObserver((o, arg) -> {
			VueEvent event = (VueEvent) arg;
			if (event instanceof StepEvent) {

				// fait les steps de N simulateur à tour de rôle
				for (int i = 0; i < simArray.length; i++) {
					stepSimulation(i);
				}

				// à chaque Step, nous prenons un simulateur pour inscrire le
				// nombre de step
				long stepLocal = simArray[0].getStep();

				EventQueue.invokeLater(() -> {
					vue.rafraichitVue(stepLocal);
				});

			} else if (event instanceof StartEvent) {
				// Réaction différente dépendant de l'état des simulations
				if (!pause) {
					stop = false;
					startSimulation();
				} else {
					resumeSimulation();
				}
			} else if (event instanceof InitEvent) {
				stop = true;

				/*
				 * Notifier les threads en pause
				 */
				synchronized (moniteurPause) {
					moniteurPause.notifyAll();
				}

				/*
				 * Notifier les threads qui on terminé leur step
				 */
				synchronized (moniteurSims) {
					moniteurSims.notifyAll();
				}

				simulationCommencer = false;
				pause = false;
				initSimulation();
			} else if (event instanceof PauseEvent) {
				if (!pause) {
					pauseSimulation();
				}
			}

		});

		// Enregistrement des adapteurs de N simulations dans la vue
		for (int i = 0; i < adapteurDeChose.length; i++) {
			vue.setFournisseur(adapteurDeChose[i]);
		}

		EventQueue.invokeLater(() -> {
			vue.initialize();
		});

	}

	/**
	 * Réinitialise la simulation. Remet la simulation dans l'état initiale
	 * prévu par les fichiers de propriétés.
	 */
	public void initSimulation() {
		simulationCommencer = true;
		// On crée de nouveau simulateurs au besoin
		for (int i = 0; i < simArray.length; i++) {
			simArray[i].init();
		}

		// On configure les simulateurs
		SimFactory.reset();
		configureSimulations();

		vue.initSimulation();

		// On synchronise la durée de vie max des plantes
		Affichable.setVieMax(Vegetal.VIE_MAX);
	}

	/**
	 * Configure les paramètres de la simulation. Lecture du fichier de
	 * propriétés et inscription des propriétés dans la simulation. Nous
	 * transférons aussi le numéro de simulateur
	 */
	public void configureSimulations() {
		for (int i = 0; i < simArray.length; i++) {
			SimFactory.reset();
			SimFactory fabrique = SimFactory.getInstance();
			simArray[i].ajouteAnimaux(fabrique
					.getAll(ChosesPossibles.ANIMAL, i));
			simArray[i].ajouteVegetaux(fabrique.getAll(ChosesPossibles.VEGETAL,
					i));
			simArray[i].ajouteChoses(fabrique.getAll(ChosesPossibles.CHOSE, i));
		}

	}

	/**
	 * Débute la simulation. Inactive tant que les threads ne sont pas utilisés
	 */
	private void startSimulation() {
		/*
		 * initie la simulation
		 */
		if (!simulationCommencer) {
			initSimulation();
			simulationCommencer = true;
		}

		/*
		 * Pour chaque simulateur, un thread est créé
		 */
		for (int i = 0; i < threadArray.length; i++) {
			Simulateur simLocal = simArray[i];
			final int numeroSimulateur = i;

			threadArray[i] = new Thread(() -> {
				boolean finished = false;

				while (simLocal.resteToujoursUnVivant() && !finished) {
					if (!pause && !stop) {

						stepSimulation(numeroSimulateur);

						try {
							Thread.sleep(vue.getPeriode());
						} catch (Exception e1) {
							e1.printStackTrace();
						}

						synchronized (moniteurThread) {
							moniteurThread.notify();
						}

						synchronized (moniteurSims) {
							try {
								moniteurSims.wait();
							} catch (Exception e) {
								e.printStackTrace();
							}
						}

						EventQueue.invokeLater(() -> {
							vue.rafraichitVue(simLocal.getStep());
						});
					} else if (stop == true) {
						finished = true;
					} else {
						synchronized (moniteurPause) {
							try {
								moniteurPause.wait();
							} catch (Exception e) {
								e.printStackTrace();
							}
						}
					}
				}

			});
		}

		/*
		 * Démare tout les threads
		 */
		for (int i = 0; i < threadArray.length; i++) {
			threadArray[i].setName("Sim_" + i);
			threadArray[i].start();
		}

		/*
		 * Thread qui s'occupe d'attendre que les simulateions termine leurs
		 * step et ensuite avertir la vue du changement. Ce thread arrete de
		 * tourner quand la simulation est stoppé
		 */
		threadFinale = new Thread(
				() -> {
					int nombreThreadTerminee = 0;
					while (nombreThreadTerminee < threadArray.length) {
						nombreThreadTerminee = 0;
						int nombreThreadPause = 0;

						if (stop) {
							nombreThreadTerminee = threadArray.length;
						} else if (pause) {
							synchronized (moniteurPause) {
								try {
									moniteurPause.wait();
								} catch (Exception e) {
									e.printStackTrace();
								}
							}
						} else {
							/*
							 * Pour chaque thread vérifie si le thread est en
							 * pause ou terminer
							 */
							for (Thread thread : threadArray) {
								if (thread.getState() == Thread.State.WAITING) {
									nombreThreadPause++;
								} else if (thread.getState() == Thread.State.TERMINATED) {
									nombreThreadTerminee++;
								}
							}

							/*
							 * Si le nombre de thread en pause est égale à la
							 * longueur de l'array afficher dans la console et
							 * reveiller les threads
							 */
							if (nombreThreadPause == threadArray.length) {

								/*
								 * Notifie les threads qui ont terminer leur
								 * step
								 */
								synchronized (moniteurSims) {
									moniteurSims.notifyAll();
								}

								/*
								 * Attend que l'un des threads termine leur
								 * tache
								 */
								synchronized (moniteurThread) {
									try {
										moniteurThread.wait();
									} catch (Exception e) {
										e.printStackTrace();
									}
								}
							}
						}
					}
				});
		/*
		 * Démare le thread finale
		 */
		threadFinale.setName("Thread Finale");
		threadFinale.start();
	}

	/**
	 * Avance la simulation d'un pas de temps
	 * 
	 * @param pNumeroSimulation
	 *            numero de simulation qui avance
	 */
	private void stepSimulation(int pNumeroSimulation) {
		if (!simulationCommencer) {
			initSimulation();
			simulationCommencer = true;
		}

		simArray[pNumeroSimulation].step();
		simArray[pNumeroSimulation].enleveMort();
		System.out.println("step " + simArray[pNumeroSimulation].getStep()
				+ "\n" + simArray[pNumeroSimulation].toString());
	}

	/**
	 * Met la simulation en pause
	 */
	private void pauseSimulation() {
		pause = true;
	}

	/**
	 * Remet la simulation en marche
	 */
	private void resumeSimulation() {
		pause = false;
		synchronized (moniteurPause) {
			moniteurPause.notifyAll();
		}
	}
}
